﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Reflection;
using System.Runtime.Serialization;

namespace Sinacor.Infra.UI.Common
{
    [Serializable]
    public abstract class ObservableObject : IObservableObject
    {
        #region Child Observable Objects
        private List<string> _observableObjectProperties;
        private List<string> ObservableObjectProperties
        {
            get
            {
                if (_observableObjectProperties == null)
                    _observableObjectProperties = new List<string>();

                return _observableObjectProperties;
            }
            set { _observableObjectProperties = value; }
        }
        #endregion

        #region IsClone
        [field: NonSerialized]
        private bool _isClone;
        public bool IsClone
        {
            get { return _isClone; }
            set { _isClone = value; }
        }
        #endregion

        #region Construtor
        public ObservableObject()
        {
            Initialize();
        }

        private void Initialize()
        {
            ((IObservableObject)this).ParentObjects = new List<IObservableObject>();

            MapObservableObjectProperties();

            this.IsObserverStarted = false;
            this.SpreadChanges = true;
            this.InternalPropertyChanged += new PropertyChangedEventHandler(ObservableObject_InternalPropertyChanged);
            this.ObserverStarted += new Action(ObservableObject_ObserverStarted);
            this.ObserverStoped += new Action(ObservableObject_ObserverStoped);
            this.ChangesCleaned += new Action(ObservableObject_ChangesCleaned);
            this.SpreadChangesChanged += new Action<bool>(ObservableObject_SpreadChangesChanged);
        }

        /// <summary>
        /// Mapear em uma collection todas as properties do objeto que implementem IObservableObject
        /// </summary>
        private void MapObservableObjectProperties()
        {
            this.ObservableObjectProperties.Clear();

            PropertyInfo[] props = this.GetType().GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

            if (props != null)
            {
                for (int i = 0; i < props.Length; i++)
                {
                    PropertyInfo p = props[i];

                    if (p.PropertyType.GetInterface("IObservableObject") != null)
                    {
                        this.ObservableObjectProperties.Add(p.Name);
                    }
                }
            }
        }

        #endregion

        #region ParentObjects
        //Collection que armazena a referencia para os "pais" do objeto
        [field: NonSerialized]
        private List<IObservableObject> _parentObjects;
        List<IObservableObject> IObservableObject.ParentObjects
        {
            get
            {
                if (_parentObjects == null)
                    _parentObjects = new List<IObservableObject>();

                return _parentObjects;
            }
            set { _parentObjects = value; }
        }
        #endregion

        #region StringEmptyIsNull
        private bool _stringEmptyIsNull = true;
        /// <summary>
        /// Propriedade que indica se strings vazias serão consideradas iguais a strings nulas
        /// Valor default=True
        /// </summary>
        public bool StringEmptyIsNull
        {
            get { return _stringEmptyIsNull; }
            set { _stringEmptyIsNull = value; }
        }
        #endregion

        #region IsObserverStarted
        /// <summary>
        /// Indica se está monitorando mudanças nas propriedades
        /// </summary>
        public bool IsObserverStarted
        {
            get;
            protected set;
        }
        #endregion

        #region HasPropertyChanges
        private bool _hasPropertyChanges;
        /// <summary>
        /// Indica se houve mudanças nas propriedades
        /// </summary>
        public bool HasPropertyChanges
        {
            get { return _hasPropertyChanges; }
            protected set
            {
                bool temp = _hasPropertyChanges;
                _hasPropertyChanges = value;
                if (temp != _hasPropertyChanges)
                {
                    if (this.PropertyChanged != null)
                        this.PropertyChanged(this, new PropertyChangedEventArgs("HasPropertyChanges"));
                }
                //SetValue("HasPropertyChanges", ref _hasPropertyChanges, value);
            }
        }
        #endregion

        #region SpreadChanges
        private bool _spreadChanges;
        /// <summary>
        /// Indica se as mudanças devem ser propagadas para os objetos pais
        /// </summary>        
        public bool SpreadChanges
        {
            get { return _spreadChanges; }
            set
            {
                bool temp = _spreadChanges;
                _spreadChanges = value;

                if (temp != value)
                {
                    if (this.SpreadChangesChanged != null)
                        this.SpreadChangesChanged(value);
                }
            }
        }
        #endregion

        #region OnDeserialized
        [OnDeserialized]
        private void OnDeserialized(StreamingContext context)
        {
            this.IsClone = true;
        }
        #endregion

        #region StartObserver
        [field: NonSerialized]
        protected internal event Action ObserverStarted;

        /// <summary>
        /// Ativa o observador de alterações do objeto
        /// </summary>
        public void StartObserver()
        {
            if (this.IsClone)
            {
                this.ReassignInternalEvents();
            }

            if (this.ObserverStarted != null)
                this.ObserverStarted();
        }

        private void ObservableObject_ObserverStarted()
        {
            this.IsObserverStarted = true;
        }

        private void ReassignInternalEvents()
        {
            this.InternalPropertyChanged += new PropertyChangedEventHandler(ObservableObject_InternalPropertyChanged);
            this.ObserverStarted += new Action(ObservableObject_ObserverStarted);
            this.ObserverStoped += new Action(ObservableObject_ObserverStoped);
            this.ChangesCleaned += new Action(ObservableObject_ChangesCleaned);
            this.SpreadChangesChanged += new Action<bool>(ObservableObject_SpreadChangesChanged);

            PropertyInfo p;
            foreach (string pName in this.ObservableObjectProperties)
            {
                p = this.GetType().GetProperty(pName, BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
                if (p != null)
                {
                    if (p.PropertyType.GetInterface("IObservableObject") != null)
                    {
                        IObservableObject value = p.GetValue(this, null) as IObservableObject;
                        if (value != null)
                        {
                            //se n for uma referencia circular "atachar" os eventos
                            if (!CheckCyclicReference(this, value))
                                this.AttachEvents(value);
                        }
                    }
                }

            }

            ////_observableProperties = new List<PropertyInfo>();
            //PropertyInfo[] props = this.GetType().GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);

            //if (props != null)
            //{
            //    for (int i = 0; i < props.Length; i++)
            //    {
            //        PropertyInfo p = props[i];

            //        if (p.PropertyType.GetInterface("IObservableObject") != null)
            //        {
            //            IObservableObject value = p.GetValue(this, null) as IObservableObject;
            //            if (value != null)
            //            {
            //                this.AttachEvents(value);
            //            }
            //        }
            //    }
            //}
        }
        #endregion

        #region StopObserver
        [field: NonSerialized]
        protected internal event Action ObserverStoped;

        /// <summary>
        /// Desativa o observador de alterações
        /// </summary>
        public void StopObserver()
        {
            if (this.ObserverStoped != null)
                this.ObserverStoped();
        }

        private void ObservableObject_ObserverStoped()
        {
            this.HasPropertyChanges = false;
            this.IsObserverStarted = false;
        }
        #endregion

        #region ClearChanges
        [field: NonSerialized]
        protected internal event Action ChangesCleaned;

        /// <summary>
        /// Limpa as mudanças do objeto
        /// </summary>
        public void ClearChanges()
        {
            if (this.ChangesCleaned != null)
                this.ChangesCleaned();
        }

        private void ObservableObject_ChangesCleaned()
        {
            this.HasPropertyChanges = false;
        }
        #endregion

        #region Observer Core
        /// <summary>
        /// Receber a notificacao de que o objeto foi alterado e mudar seu estado
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ObservableObject_InternalPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (this.IsObserverStarted && e.PropertyName != "HasPropertyChanges")
            {
                if (this.SpreadChanges || (!this.SpreadChanges && sender == this))
                    this.HasPropertyChanges = true;

                if (sender != this)
                    if (this.PropertyChanged != null)
                        this.PropertyChanged(sender, e);
            }
        }

        /// <summary>
        /// Verificar se houve alteracoes
        /// </summary>
        /// <param name="propName"></param>
        /// <param name="field"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        private bool CheckChanges(object field, object value)
        {
            bool hasChanges = false;

            if (field is IComparable || value is IComparable)
            {
                //tratamento especifico para strings
                if (this.StringEmptyIsNull && (field is string || value is string))
                {
                    if (string.IsNullOrEmpty((string)field) && string.IsNullOrEmpty((string)value))
                        return false;
                }

                if (field == null && value == null)
                    hasChanges = false;
                else if (field != null)
                    hasChanges = (((IComparable)field).CompareTo(value) != 0);
                else if (value != null)
                    hasChanges = (((IComparable)value).CompareTo(field) != 0);
                else
                    hasChanges = (((IComparable)field).CompareTo(value) != 0);
            }
            else
                hasChanges = (field != value);

            return hasChanges;
        }

        /// <summary>
        /// Atribuir um valor a um field e notificar se ha alteracoes quando o observador estiver ativo
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="propName"></param>
        /// <param name="field"></param>
        /// <param name="value"></param>
        public void SetValue<TValue>(string propName, ref TValue field, TValue value)
        {
            object originalFieldValue = field;
            field = value;

            //caso seja uma sub-model e seja um observable object
            if (field is IObservableObject)
            {
                if (!(field as IObservableObject).ParentObjects.Contains(this))
                    if (!CheckCyclicReference(this, field as IObservableObject))
                        this.AttachObservableObject((IObservableObject)field);
            }

            // Se valor antigo for um IObservableObject e valor mudou, deve 
            // desatachar o valor antigo
            if (originalFieldValue is IObservableObject &&
                CheckChanges(originalFieldValue, value))
            {
                this.DetachObservableObject((IObservableObject)originalFieldValue);
            }

            //verificar se houve alteracoes de disparar o evento
            if (CheckChanges(originalFieldValue, value))
            {
                if (this.IsObserverStarted)
                    this.HasPropertyChanges = true;

                this.OnPropertyChanged(propName);
            }
        }

        /// <summary>
        /// Verifica se ha referencia circular
        /// </summary>
        /// <param name="field"></param>
        /// <returns>
        /// True  - há referência circular
        /// False - não há referência circular
        /// </returns>
        protected static bool CheckCyclicReference(IObservableObject parent, IObservableObject child)
        {
            if (parent.ParentObjects.Contains(child as IObservableObject))
                return true;
            else
            {
                foreach (IObservableObject o in parent.ParentObjects)
                {
                    if (CheckCyclicReference(o, child))
                        return true;
                }
            }

            return false;
        }

        private void AttachObservableObject(IObservableObject obj)
        {
            AttachEvents(obj);

            if (this.IsObserverStarted)
                obj.StartObserver();
            else
                obj.StopObserver();

            obj.SpreadChanges = this.SpreadChanges;

            if (this.IsObserverStarted && this.SpreadChanges && obj.HasPropertyChanges)
            {
                this.HasPropertyChanges = true;
                this.OnPropertyChanged("");
            }
        }

        private void AttachEvents(IObservableObject obj)
        {
            DetachEvents(obj);

            //adicionar o objeto a lista de "pais" do objeto sendo "atachado"
            if (!obj.ParentObjects.Contains(this))
                obj.ParentObjects.Add(this);

            //assinar os eventos de start e stop do observador de forma a propragar a
            //configuracao para as submodels
            this.ObserverStarted += new Action(obj.StartObserver);
            this.ObserverStoped += new Action(obj.StopObserver);
            this.ChangesCleaned += new Action(obj.ClearChanges);

            this.SpreadChangesChanged += new Action<bool>(obj.OnSpreadChangesChanged);

            //assinar o evento property changed do objeto pai de forma a propagar a mudanca
            //de estado do objeto de mais filho para o pai
            //if (this.InternalPropertyChanged != null)
            //    obj.InternalPropertyChanged += new PropertyChangedEventHandler(this.InternalPropertyChanged);
            obj.InternalPropertyChanged += new PropertyChangedEventHandler(this.ObservableObject_InternalPropertyChanged);
        }


        private void DetachObservableObject(IObservableObject obj)
        {
            DetachEvents(obj);
        }

        private void DetachEvents(IObservableObject obj)
        {
            //adicionar o objeto a lista de "pais" do objeto sendo "atachado"
            if (obj.ParentObjects.Contains(this))
                obj.ParentObjects.Remove(this);

            //assinar os eventos de start e stop do observador de forma a propragar a
            //configuracao para as submodels
            if (this.ObserverStarted != null)
                this.ObserverStarted -= new Action(obj.StartObserver);

            if (this.ObserverStoped != null)
                this.ObserverStoped -= new Action(obj.StopObserver);

            if (this.ChangesCleaned != null)
                this.ChangesCleaned -= new Action(obj.ClearChanges);

            if (this.SpreadChangesChanged != null)
                this.SpreadChangesChanged -= new Action<bool>(obj.OnSpreadChangesChanged);

            //assinar o evento property changed do objeto pai de forma a propagar a mudanca
            //de estado do objeto de mais filho para o pai
            //como nao e possivel verificar se o evento do objeto filho esta assinado
            //try
            //{
            //if (this.InternalPropertyChanged != null)
            //    obj.InternalPropertyChanged -= new PropertyChangedEventHandler(this.InternalPropertyChanged);
            // }
            //catch
            //{
            //}
            obj.InternalPropertyChanged -= new PropertyChangedEventHandler(this.ObservableObject_InternalPropertyChanged);
        }

        void ObservableObject_SpreadChangesChanged(bool value)
        {
            this.SpreadChanges = value;
        }

        [field: NonSerialized]
        public event PropertyChangedEventHandler InternalPropertyChanged;

        [field: NonSerialized]
        public event Action<bool> SpreadChangesChanged;

        void IObservableObject.OnSpreadChangesChanged(bool newValue)
        {
            if (this.SpreadChangesChanged != null)
                this.SpreadChangesChanged(newValue);
        }

        #endregion

        #region INotifyPropertyChanged Members
        /// <summary>
        /// Evento disparado quando uma property é alterada
        /// </summary>
        [field: NonSerialized]
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Dispara o evento <c>PropertyChanged</c>
        /// </summary>
        /// <param name="propertyName">Nome da property</param>
        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);

            if (this.InternalPropertyChanged != null)
            {
                InternalPropertyChanged(this, e);
            }

            if (this.PropertyChanged != null)
            {
                PropertyChanged(this, e);
            }
        }
        #endregion
    }
}
